﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
using nRoute.Components.Messaging;
using nRoute.Utilities;
#if (SILVERLIGHT)
using System.Windows;
using System.Windows.Resources;
using System.Xml;
#endif

namespace nRoute.Mapping
{
	public static class RemoteResourceLoader
    {
        private const string DLL_EXT = ".dll";
        private const string XAP_EXT = ".xap";
        private const string DLLXAP_ONLY_MESSAGE = "RemoteResourceLoaded can only download, dll or xap files. '{0}' cannot be loaded";
        private const string DLL_ONLY_MESSAGE = "RemoteResourceLoaded can only download dll files. '{0}' cannot be loaded";
        private const string APPMANIFIEST_NAME = "AppManifest.xaml";
        private const string ASSEMPLYPART_XMLTOKEN = "AssemblyPart";
        private const string SOURCE_XMLTOKEN = "Source";
        private const string BINARYTYPE_NAME = "application/binary";

        private readonly static List<Uri> _loadingPackages;
        private readonly static List<Uri> _loadedPackages;
        private readonly static Object _lock = new Object();

        static RemoteResourceLoader()
        {
            _loadedPackages = new List<Uri>();
            _loadingPackages = new List<Uri>();
        }

#region Main Methods

        public static bool IsResourceLoaded(Uri resourceUri)
        {
            Guard.ArgumentNotNull(resourceUri, "resourceUri");
            lock (_lock)
            {
                return _loadedPackages.Any((u) => IsUriEqual(u, resourceUri));
            }
        }
        
        public static IObservable<LoadedResourceInfo> LoadResource(Uri resourceUri)
        {
            Guard.ArgumentNotNull(resourceUri, "resourceUri");
#if SILVERLIGHT
            Guard.ArgumentValue((resourceUri.ToString().EndsWith(XAP_EXT, StringComparison.OrdinalIgnoreCase) == false &&
               resourceUri.ToString().EndsWith(DLL_EXT, StringComparison.OrdinalIgnoreCase) == false), "resourceUri",
               DLLXAP_ONLY_MESSAGE, resourceUri.ToString());
#else
            Guard.ArgumentValue((resourceUri.ToString().EndsWith(DLL_EXT, StringComparison.OrdinalIgnoreCase) == false), "resourceUri",
               DLL_ONLY_MESSAGE, resourceUri.ToString());
#endif

            // earlier
            //var _loader = new LazyRelayObservable<LoadedResourceInfo>((o) =>

            var _loader = Observable.Create<LoadedResourceInfo>((o) =>
            {
                lock (_lock)
                {
                    // if already loaded, don't do anything
                    if (_loadedPackages.Any((u) => IsUriEqual(u, resourceUri)))
                    {
                        o.OnNext(new LoadedResourceInfo(resourceUri, true, null));
                        o.OnCompleted();
                        return Disposable.Empty;
                    }

                    // if loading, we wait for it to be loaded
                    if (_loadingPackages.Any((u) => IsUriEqual(u, resourceUri)))
                    {
                        var _token = (IDisposable) null;
                        _token = Channel<LoadedResourceInfo>.Public.Subscribe((r) =>
                        {
                            if (IsUriEqual(r.ResourceUri, resourceUri))
                            {
                                o.OnNext(r);
                                o.OnCompleted();
                                _token.Dispose();
                            }
                        });
                        return _token;
                    }

                    // else add that we are loading
                    _loadingPackages.Add(resourceUri);
                }

#if (SILVERLIGHT)

                var _webClient = new WebClient();
                _webClient.OpenReadCompleted += (s, e) =>
                {
                    var _remoteResourceInfo = new LoadedResourceInfo(resourceUri, (!e.Cancelled && e.Error == null), e.Error);
                    lock (_lock)
                    {
                        _loadingPackages.Remove(resourceUri);

                        if (_remoteResourceInfo.IsLoaded)
                        {

                            _loadedPackages.Add(resourceUri);
                            if (resourceUri.ToString().EndsWith(XAP_EXT, StringComparison.OrdinalIgnoreCase))
                            {
                                ProcessPackageDownload(e.Result);
                            }
                            else
                            {
                                ProcessAssemblyDownload(e.Result);
                            }
                        }
                    }

                    Channel<LoadedResourceInfo>.Public.OnNext(_remoteResourceInfo);
                    o.OnNext(_remoteResourceInfo);
                    o.OnCompleted();
                };

                _webClient.OpenReadAsync(resourceUri);

#else

                if (resourceUri.IsAbsoluteUri)
                {
                    // and we load
                    var _webClient = new WebClient();
                    _webClient.DownloadDataCompleted += (s, e) =>
                    {
                        var _remoteResourceInfo = new LoadedResourceInfo(resourceUri, (!e.Cancelled && e.Error == null), e.Error);
                        lock (_lock)
                        {
                            _loadingPackages.Remove(resourceUri);

                            if (_remoteResourceInfo.IsLoaded)
                            {
                                _loadedPackages.Add(resourceUri);
                                ProcessAssemblyDownload(e.Result);
                            }
                        }

                        Channel<LoadedResourceInfo>.Public.OnNext(_remoteResourceInfo);
                        o.OnNext(_remoteResourceInfo);
                        o.OnCompleted();
                    };

                    _webClient.DownloadDataAsync(resourceUri);                    
                }
                else
                {
                    var _remoteResourceInfo = (LoadedResourceInfo)null;
                    try
                    {
                        lock (_lock)
                        {
                            _loadingPackages.Remove(resourceUri);

                            var _fileName = resourceUri.OriginalString;
                            var _filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _fileName);
                            var _assembly = Assembly.LoadFile(_filePath);
                            _loadedPackages.Add(resourceUri);

                            AssemblyMapper.MapAssembly(_assembly);
                            _remoteResourceInfo = new LoadedResourceInfo(resourceUri, true, null);
                        }
                    }
                    catch (Exception ex)
                    {
                        _remoteResourceInfo = new LoadedResourceInfo(resourceUri, false, ex);
                    }

                    Channel<LoadedResourceInfo>.Public.OnNext(_remoteResourceInfo);
                    o.OnNext(_remoteResourceInfo);
                    o.OnCompleted();
                }
#endif
                return Disposable.Empty;
            });

            return _loader;
        }

#endregion

#region Helpers

#if SILVERLIGHT
        private static void ProcessAssemblyDownload(Stream stream)
        {
            // create a assembly part to load
            AssemblyPart _assemblyPart = new AssemblyPart();
            Assembly _assembly = _assemblyPart.Load(stream);

            // and map the assembly
            AssemblyMapper.MapAssembly(_assembly);
        }
#elif (!WINRT)
        private static void ProcessAssemblyDownload(Byte[] data)
        {
            // and map the assembly
            var _assembly = Assembly.Load(data);
            AssemblyMapper.MapAssembly(_assembly);
        }
#endif

#if SILVERLIGHT

        private static void ProcessPackageDownload(Stream stream)
        {
            // ideas from http://pagebrooks.com/archive/2009/05/17/a-smaller-xap-preloader-for-silverlight.aspx
            // load from app manifest the zip stream

            var _manifestStream = Application.GetResourceStream(
                new StreamResourceInfo(stream, null),
                new Uri(APPMANIFIEST_NAME, UriKind.Relative));

            // create a xml document
            var _manifest = new StreamReader(_manifestStream.Stream).ReadToEnd();
            var _reader = XmlReader.Create(new StringReader(_manifest));
            var _loadedAssemblies = new List<Assembly>();

            // read all the AssemblyPart elements
            while (_reader.Read())
            {
                if (_reader.IsStartElement(ASSEMPLYPART_XMLTOKEN))
                {
                    // we get the source attribute which has the dll file name
                    _reader.MoveToAttribute(SOURCE_XMLTOKEN);
                    _reader.ReadAttributeValue();

                    // we get the assembly
                    Assembly _assembly = null;
                    var _assemblyStream = new StreamResourceInfo(stream, BINARYTYPE_NAME);
                    var _stream = Application.GetResourceStream(_assemblyStream, new Uri(_reader.Value, UriKind.Relative));

                    // create a assembly part to load
                    AssemblyPart _assemblyPart = new AssemblyPart();
                    _assembly = _assemblyPart.Load(_stream.Stream);

                    // we add to the list of loaded assemblies
                    _loadedAssemblies.Add(_assembly);
                }
            }

            // note we first load all assemblies, then only we map - this is important because assemblies can have 
            // dependencies on other assemblies, and if they are not loaded they can't be resolved i.e. will throw errors
            foreach (var _assembly in _loadedAssemblies)
            {
                AssemblyMapper.MapAssembly(_assembly);
            }
        }

#endif

        private static bool IsUriEqual(Uri url1, Uri url2)
        {
            if (url1 == null && url2 == null) return true;
            if (url1 == null || url2 == null) return false;
            return (Uri.Compare(url1, url2, UriComponents.HttpRequestUrl, UriFormat.UriEscaped, Utilities.Compatability.INVARIANT_IGNORECASE_STRING_COMPARISION) == 0);
        }

#endregion

    }
}